<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage groove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

class RootLexer extends Lexer {

    public function tokenize() {
        $buffer = null;
        $capture = null;
        $expectEquals = false;
        $expectString = false;

        while(($next = $this->inputStream->read()) !== -1) {
            if($next === '/') {
                $lexer = new CommentLexer($this->inputStream, $this->parser);
                $lexer->tokenize();
                continue;
            }

            if(ctype_space($next)) {
                continue;
            }

            if($next === '{') {
                $this->parser->parse(new GrooveToken(GrooveToken :: T_CURLY_OPEN, $next));

                $lexer = new TemplateLexer($this->inputStream, $this->parser);
                $lexer->tokenize();
                continue;
            }

            try {
                $sink = new TokenSink();
                $lexer = new ContextLexer($this->inputStream, $sink);

                $this->inputStream->regress(1);

                $offset = $this->inputStream->offset();

                $lexer->tokenize();
                $sink->clear($this->parser);

                continue;
            } catch(TokenException $e) {
                $this->inputStream->seek($offset);
            }

            try {
                $sink = new TokenSink();
                $lexer = new RuleLexer($this->inputStream, $sink);

                $offset = $this->inputStream->offset();

                $lexer->tokenize();
                $sink->clear($this->parser);
                $this->inputStream->regress(1);

                continue;
            } catch(TokenException $e) {
                $this->inputStream->seek($offset);
            }

             
            try {
                $sink = new TokenSink();
                $lexer = new UseLexer($this->inputStream, $sink);

                $offset = $this->inputStream->offset();

                $lexer->tokenize();
                $sink->clear($this->parser);

                continue;
            } catch(TokenException $e) {
                $this->inputStream->seek($offset);
            }

            try {
                $sink = new TokenSink();
                $lexer = new NamespaceLexer($this->inputStream, $sink);

                $offset = $this->inputStream->offset();

                $lexer->tokenize();
                $sink->clear($this->parser);

                continue;
            } catch(TokenException $e) {
                $this->inputStream->seek($offset);
            }

            throw new TokenException('Unexpected character \'' . $next . '\'');
        }
    }
}

class RuleLexer extends Lexer {

    public function tokenize() {
        $buffer = null;
        $capture = null;

        $allowNamed = true;
        $isNamed = false;

        $expectEquals = false;
        $expectString = false;

        while(($next = $this->inputStream->read()) !== -1) {
            if($buffer !== null) {
                if($next === '_' || $next === '\\' || ctype_alnum($next)) {
                    if($isNamed) {
                        if(mb_strlen($buffer) > 2) {
                            if($next === '_' || ctype_alnum($next)) {
                                $buffer .= $next;
                                continue;
                            }
                        } else {
                            if($next === '_' || ctype_alpha($next)) {
                                $buffer .= $next;
                                continue;
                            }
                        }
                        throw new TokenException("Unexpected token " . $next);
                    } else {
                        $buffer .= $next;
                        if(mb_stristr($buffer, '\\\\')) {
                            throw new TokenException("Unexpected namespace separator");
                        }
                    }
                    continue;
                } else {
                    if($isNamed && mb_strlen($buffer) === 1) {
                        throw new TokenException("Empty @ name");
                    }

                    $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                    $buffer = null;
                    $allowNamed = true;
                    $isNamed = false;
                    $expectString = false;
                }
            }

            if($capture !== null) {
                if($next === '_' || ctype_alnum($next)) {
                    $capture .= $next;
                    continue;
                } else {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_VARIABLE, $capture));
                    $capture = null;
                    $expectEquals = true;
                    $allowNamed = false;
                }
            }

            if($expectEquals) {
                if(ctype_space($next)) {
                    continue;
                }
                if($next === '=') {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_ASSIGNMENT, '='));
                    $expectEquals = false;
                    $expectString = true;
                    continue;
                }
                throw new TokenException('Unexpected character \'' . $next . '\', expected \'=\'');
            }

            if($expectString) {
                if(!($next === '_' || $next === '\\' || ctype_alpha($next) || ctype_space($next))) {
                    throw new TokenException('Unexpected character \'' . $next . '\'');
                }
            }

            if($next === '_' || $next === '\\' || ctype_alpha($next)) {
                $buffer = $next;
                continue;
            }

            if($next === '@' && $allowNamed) {
                $isNamed = true;
                $buffer = $next;
                continue;
            }

            if($next === '$') {
                $capture = $next;
                continue;
            }

            if($next === ',') {
                $this->parser->parse(new GrooveToken(GrooveToken :: T_MATCH_SEPARATOR, $next));
                continue;
            }

            if($next === '{') {
                return;
            }

            if(ctype_space($next)) {
                continue;
            }

            throw new TokenException('Unexpected character \'' . $next . '\'');
        }
    }
}

class ContextLexer extends Lexer {

    public function tokenize() {
        $expectVariable = true;
        $expectAssignment = false;
        $expectString = false;
        $expectSemicolon = false;
        $buffer = null;

        while(($next = $this->inputStream->read()) !== -1) {

            if($expectVariable) {
                if($buffer === null) {
                    if(ctype_space($next)) {
                        continue;
                    }

                    if($next === '$') {
                        $buffer = '$';
                        continue;
                    }

                    throw new TokenException('Unexpected character \'' . $next . '\'');
                }

                if($buffer === '$') {
                    if(ctype_alpha($next) || $next === '_') {
                        $buffer .= $next;
                        continue;
                    }

                    throw new TokenException('Unexpected character \'' . $next . '\'');
                }

                 
                if(ctype_alnum($next) || $next === '_') {
                    $buffer .= $next;
                    continue;
                }

                $this->parser->parse(new GrooveToken(GrooveToken :: T_CONTEXT, $buffer));
                $this->parser->parse(new GrooveToken(GrooveToken :: T_VARIABLE, $buffer));

                $buffer = null;

                $expectVariable = false;
                $expectAssignment = true;
            }

            if($expectAssignment) {
                if(ctype_space($next)) {
                    continue;
                }

                if($next === '=') {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_ASSIGNMENT, $next));
                    $expectAssignment = false;
                    $expectString = true;
                    continue;
                }

                throw new TokenException('Unexpected character \'' . $next . '\'');
            }

            if($expectString) {
                if($buffer === null) {
                    if(ctype_space($next)) {
                        continue;
                    }

                    if($next === '_' || $next === '\\' || ctype_alpha($next)) {
                        $buffer = $next;
                        continue;
                    }

                    throw new TokenException('Unexpected character \'' . $next . '\'');
                }

                if($next === '_' || $next === '\\' || ctype_alnum($next)) {
                    $buffer .= $next;
                    if(mb_stristr($buffer, '\\\\')) {
                        throw new TokenException("Unexpected namespace separator");
                    }
                    continue;
                } else {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                    $buffer = null;
                    $expectString = false;
                    $expectSemicolon = true;
                }
            }

            if($expectSemicolon) {
                if(ctype_space($next)) {
                    continue;
                }

                if($next === ';') {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_SEMICOLON, $next));
                    return;
                }

                throw new TokenException('Unexpected character \'' . $next . '\'');
            }

            throw new TokenException('Unexpected character \'' . $next . '\'');
        }
    }
}

class UseLexer extends Lexer {

    public function tokenize() {
        $buffer = null;

        $expectUse = true;
        $expectImport = false;
        $expectAsOrNext = false;
        $expectName = false;
        $expectNext = false;

        while(($next = $this->inputStream->read()) !== -1) {
             
            // ^\s*use
            if($expectUse) {
                if(ctype_space($next)) {
                    if($buffer === null) {
                        continue;
                    }

                    if(mb_strtolower($buffer) === 'use') {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_USE, $buffer));
                        $buffer = null;
                        $expectUse = false;
                        $expectImport = true;
                        continue;
                    }
                }

                if($buffer === null) {
                    $buffer = $next;
                } else {
                    $buffer .= $next;
                }

                if(mb_substr('use', 0, mb_strlen($buffer)) !== mb_strtolower($buffer)) {
                    throw new TokenException("Expected namespace, got {$buffer}");
                }

                continue;
            }

            // [a-z_\\][a-z_0-9\\]*
            if($expectImport) {
                if($buffer === null) {
                    if(ctype_space($next)) {
                        continue;
                    }

                    if(ctype_alpha($next) || $next === '\\' || $next === '_') {
                        $buffer = $next;
                        continue;
                    }
                } else {
                    if(ctype_alnum($next) || $next === '\\' || $next === '_') {
                        $buffer .= $next;
                        if(mb_substr($buffer, -2) === '\\\\') {
                            throw new TokenException("Unexpected namespace separator");
                        }
                        continue;
                    }

                    if(ctype_space($next)) {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                        $buffer = null;
                        $expectImport = false;
                        $expectAsOrNext = true;
                        continue;
                    }

                    if($next === ';') {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_SEMICOLON, ';'));
                        return;
                    }

                    if($next === ',') {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_COMMA, ','));
                        $buffer = null;
                        continue;
                    }
                }

            }

            // (as|;|,)
            if($expectAsOrNext) {
                if($buffer === null) {
                    if(ctype_space($next)) {
                        continue;
                    }
                    if($next === ';') {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_SEMICOLON, ';'));
                        return;
                    }
                    if($next === ',') {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_COMMA, ','));
                        $expectAsOrNext = false;
                        $expectImport = true;
                        continue;
                    }
                    if(mb_strtolower($next) === 'a') {
                        $buffer = $next;
                        continue;
                    }
                } else {
                    if(ctype_space($next)) {
                        if(mb_strtolower($buffer) === 'as') {
                            $this->parser->parse(new GrooveToken(GrooveToken :: T_AS, $buffer));
                            $buffer = null;
                            $expectAsOrNext = false;
                            $expectName = true;
                            continue;
                        } else {
                            throw new TokenException('Expected \'as\'');
                        }
                    }

                    $buffer .= $next;

                    if(mb_substr('as', 0, mb_strlen($buffer)) !== mb_strtolower($buffer)) {
                        throw new TokenException("Expected as, got {$buffer}");
                    }

                    continue;
                }
            }

            if($expectName) {
                if($buffer === null) {
                    if(ctype_alpha($next) || $next === '_') {
                        $buffer = $next;
                        continue;
                    }
                } else {
                    if(ctype_alnum($next) || $next === '_') {
                        $buffer .= $next;
                        continue;
                    }

                    if(ctype_space($next)) {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));

                        $buffer = null;
                        $expectNext = true;
                        $expectName = false;

                        continue;
                    }

                    if($next === ';') {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_SEMICOLON, $next));

                        return;
                    }

                    if($next === ',') {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_COMMA, $next));

                        $buffer = null;
                        $expectImport = true;
                        $expectName = false;

                        continue;;
                    }
                }
            }

            if($expectNext) {
                if(ctype_space($next)) {
                    continue;
                }
                if($next === ';') {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_SEMICOLON, $next));
                    return;
                }
                if($next === ',') {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_COMMA, $next));
                    $expectNext = false;
                    $expectImport = true;
                    continue;
                }
            }

            throw new TokenException('Unexpected character \'' . $next . '\'');
        }
    }
}

class NamespaceLexer extends Lexer {

    public function tokenize() {
        $buffer = null;

        $namespace = 'namespace';

        $expectNamespace = true;
        $expectSemicolon = false;

        while(($next = $this->inputStream->read()) !== -1) {
            if($expectNamespace) {
                if(ctype_space($next)) {
                    if($buffer === null) {
                        continue;
                    }
                    if($buffer === $namespace) {
                        $this->parser->parse(new GrooveToken(GrooveToken :: T_NAMESPACE, $buffer));
                        $buffer = null;
                        $expectNamespace = false;
                        continue;
                    }
                }

                if($buffer === null) {
                    $buffer = $next;
                } else {
                    $buffer .= $next;
                }

                if(mb_substr($namespace, 0, mb_strlen($buffer)) !== mb_strtolower($buffer)) {
                    throw new TokenException("Expected namespace, got {$buffer}");
                }

                continue;
            }
             
            if($expectSemicolon) {
                if(ctype_space($next)) {
                    continue;
                }

                if($next !== ';') {
                    throw new TokenException("Expected semicolon, got {$next}");
                }

                $this->parser->parse(new GrooveToken(GrooveToken :: T_SEMICOLON, ';'));

                return;
            }

            if($buffer !== null) {
                if($next === '_' || ctype_alnum($next) || $next === '\\') {
                    $buffer .= $next;
                    if(mb_substr($buffer, -2) === '\\\\') {
                        throw new TokenException("Unexpected namespace separator");
                    }
                    continue;
                } elseif(ctype_space($next)) {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                    $buffer = null;
                    $expectSemicolon = true;
                } elseif($next === ';') {
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_STRING, $buffer));
                    $buffer = null;
                    $this->parser->parse(new GrooveToken(GrooveToken :: T_SEMICOLON, ';'));
                    return;
                }
            }

            if($next === '_' || ctype_alpha($next)) {
                $buffer = $next;
                continue;
            }

            if(ctype_space($next)) {
                continue;
            }

            throw new TokenException('Unexpected character \'' . $next . '\'');
        }
    }
}
